pandera - инструмент проверки данных, который предоставляет интуитивно понятный, гибкий и выразительный API для проверки структур данных pandas во время выполнения.
#!pip3 install pandera
#conda install -c conda-forge pandera
Начем с показательного примера:
import pandas as pd
import pandera as pa
# создадим фрейм данных:
df = pd.DataFrame({
"column1": [1, 4, 0, 10, 9],
"column2": [-1.3, -1.4, -2.9, -10.1, -20.4],
"column3": ["value_1", "value_2", "value_3", "value_2", "value_1"]
})
df
column1 | column2 | column3 | |
---|---|---|---|
0 | 1 | -1.3 | value_1 |
1 | 4 | -1.4 | value_2 |
2 | 0 | -2.9 | value_3 |
3 | 10 | -10.1 | value_2 |
4 | 9 | -20.4 | value_1 |
# определим схему для проверки фрейма данных:
schema = pa.DataFrameSchema({
"column1": pa.Column(int, checks=pa.Check.le(10)), # Проверим, что значения меньше или равны 10
"column2": pa.Column(float, checks=pa.Check.lt(-1.2)), # Проверим, что значения ряда строго меньше -1.2
"column3": pa.Column(str, checks=[
pa.Check.str_startswith("value_"),
# определим пользовательские проверки как функции,
# которые принимают серию в качестве входных данных
pa.Check(lambda s: s.str.split("_", expand=True).shape[1] == 2)
]),
})
schema(df)
# ошибок не произошло, значит проверка прошла успешно!
column1 | column2 | column3 | |
---|---|---|---|
0 | 1 | -1.3 | value_1 |
1 | 4 | -1.4 | value_2 |
2 | 0 | -2.9 | value_3 |
3 | 10 | -10.1 | value_2 |
4 | 9 | -20.4 | value_1 |
Основные понятия pandera - schemas
(схемы), schema components
(компоненты схемы) и checks
(чекеры).
Схемы - это вызываемые объекты, которые инициализируются правилами проверки. При вызове с совместимыми данными в качестве входного аргумента объект схемы возвращает сами данные, если проверка проходит успешно или вызывает ошибку SchemaError
.
Компоненты схемы ведут себя так же, как схемы, но в основном используются для определения правил проверки для определенных частей объекта pandas, например столбцов во фрейме данных.
Наконец, чекеры позволяют пользователям формулировать правила проверки в зависимости от типа данных, которые схема или компонент схемы могут проверить.
В частности, центральными объектами pandera являются DataFrameSchema
, Column
и Check
. Вместе эти объекты позволяют пользователям заранее выражать схемы в виде контрактов логически сгруппированных наборов правил проверки, которые работают с фреймами данных pandas.
Например, рассмотрим простой набор данных, содержащий данные о людях, где каждая строка - это человек, а каждый столбец - атрибут об этом человеке:
import pandas as pd
dataframe = pd.DataFrame({
"person_id": [1, 2, 3, 4],
"height_in_feet": [6.5, 7, 6.1, 5.1],
"date_of_birth": pd.to_datetime([
"2005", "2000", "1995", "2000",
]),
"education": [
"highschool", "undergrad", "grad", "undergrad",
],
})
dataframe
person_id | height_in_feet | date_of_birth | education | |
---|---|---|---|---|
0 | 1 | 6.5 | 2005-01-01 | highschool |
1 | 2 | 7.0 | 2000-01-01 | undergrad |
2 | 3 | 6.1 | 1995-01-01 | grad |
3 | 4 | 5.1 | 2000-01-01 | undergrad |
Изучив имена столбцов и значения данных, можем заметить, что возможно привнести некоторые знания о мире в предметную область, чтобы выразить наши предположения о том, что считать достоверными данными:
import pandas as pd
import pandera as pa
from pandera import Column
typed_schema = pa.DataFrameSchema(
{
"person_id": Column(pa.Int),
# поддерживаются типы данных numpy и pandas
"height_in_feet": Column("float"),
"date_of_birth": Column("datetime64[ns]"),
"education": Column(
pd.StringDtype(),
nullable=True
),
},
# принудительное преобразование к типам данных при проверке фрейма
coerce=True
)
typed_schema(dataframe)
# возвращается фрейм данных
person_id | height_in_feet | date_of_birth | education | |
---|---|---|---|---|
0 | 1 | 6.5 | 2005-01-01 | highschool |
1 | 2 | 7.0 | 2000-01-01 | undergrad |
2 | 3 | 6.1 | 1995-01-01 | grad |
3 | 4 | 5.1 | 2000-01-01 | undergrad |
Приведенная выше typed_schema
просто проверяет столбцы, которые, как ожидается, будут присутствовать в допустимом фрейме данных, и связанные с ними типы данных.
Пользователи могут пойти дальше, сделав утверждения о значениях, которые заполняют эти столбцы:
import pandas as pd
import pandera as pa
from pandera import Column, Check
checked_schema = pa.DataFrameSchema(
{
"person_id": Column(
pa.Int,
Check.greater_than(0), # значения ряда строго больше 0
# https://pandera.readthedocs.io/en/stable/generated/methods/pandera.checks.Check.greater_than.html
allow_duplicates=False,
),
"height_in_feet": Column(
"float",
Check.in_range(0, 10), # все значения ряда находятся в пределах интервала (0, 10)
# https://pandera.readthedocs.io/en/stable/generated/methods/pandera.checks.Check.in_range.html
),
"date_of_birth": Column(
"datetime64[ns]",
Check.less_than_or_equal_to( # значения меньше или равны pd.Timestamp.now()
# https://pandera.readthedocs.io/en/stable/generated/methods/pandera.checks.Check.less_than_or_equal_to.html
pd.Timestamp.now()
),
),
"education": Column(
pd.StringDtype(),
Check.isin([ # в серии встречаются только допустимые значения из списка
# https://pandera.readthedocs.io/en/stable/generated/methods/pandera.checks.Check.isin.html
"highschool",
"undergrad",
"grad",
]),
nullable=True,
),
},
coerce=True
)
checked_schema(dataframe)
# возвращается фрейм данных
person_id | height_in_feet | date_of_birth | education | |
---|---|---|---|---|
0 | 1 | 6.5 | 2005-01-01 | highschool |
1 | 2 | 7.0 | 2000-01-01 | undergrad |
2 | 3 | 6.1 | 1995-01-01 | grad |
3 | 4 | 5.1 | 2000-01-01 | undergrad |
Приведенное выше определение схемы устанавливает следующие свойства данных:
person_id
представляет собой положительное целое число, которое является распространенным способом кодирования уникальных идентификаторов в наборе данных. Установив для allow_duplicates
значение False
, схема указывает, что этот столбец является уникальным идентификатором в этом набор данных.height_in_feet
- положительное число с плавающей точкой, максимальное значение составляет 10 футов
, что является разумным предположением для максимального роста человека.date_of_birth
не может быть датой в будущем.education
может принимать допустимые значения в наборе {"highschool", "undergrad", "grad"}
. Предположим, что эти данные были собраны в онлайн-форме, где ввод поля был необязательным, было бы целесообразно установить nullable
как True
(по умолчанию этот аргумент равен False
).Если фрейм данных, переданный в вызываемый объект схемы (schema), не проходит проверки, pandera выдает информативное сообщение об ошибке:
# данные, которые не проходят проверку:
invalid_dataframe = pd.DataFrame({
"person_id": [6, 7, 8, 9],
"height_in_feet": [-10, 20, 20, 5.1],
"date_of_birth": pd.to_datetime([
"2005", "2000", "1995", "2000",
]),
"education": [
"highschool", "undergrad", "grad", "undergrad",
],
})
checked_schema(invalid_dataframe)
Ошибка:
SchemaError: <Schema Column(name=height_in_feet, type=float)> failed element-wise validator 0:
<Check in_range: in_range(0, 10)>
failure cases:
index failure_case
0 0 -10.0
1 1 20.0
Причины ошибки SchemaError
отображаются в виде фрейма данных, где индекс failure_case
- это конкретное значение данных, которое не соответствует правилу проверки Check.in_range
, столбец индекса содержит список местоположений индекса в недействительном фрейме данных с ошибочными значениями, а столбец count
суммирует количество случаев сбоя этого конкретного значения.
Для более тонкой отладки аналитик может перехватить исключение с помощью шаблона try ... except
для доступа к данным и случаям сбоя в качестве атрибутов в объекте SchemaError
:
from pandera.errors import SchemaError
try:
checked_schema(invalid_dataframe)
except SchemaError as e:
print("Failed check:", e.check)
print("\nInvalidated dataframe:\n", e.data)
print("\nFailure cases:\n", e.failure_cases)
Таким образом, пользователи могут легко получить доступ и проверить недопустимый фрейм данных и случаи сбоя, что особенно полезно в контексте длинных цепочек методов преобразования данных:
raw_data = ... # получение сырых данных
schema = ... # определение схемы
try:
clean_data = (
raw_data
.rename(...)
.assign(...)
.groupby(...)
.apply(...)
.pipe(schema)
)
except SchemaError as e:
# e.data будет содержать итоговый фрейм данных
# для вызова groupby().apply()
...
Проверка гипотезы
Чтобы предоставить специалистам полнофункциональный инструмент проверки данных, pandera наследует подклассы от класса Check
для определения Hypothesis
с целью выражения проверок статистических гипотез.
Чтобы проиллюстрировать один из вариантов использования этой функции, рассмотрим игрушечное научное исследование, в котором контрольная группа получает плацебо, а лечебная группа получает лекарство, которое, как предполагается, улучшает физическую выносливость. Затем участники этого исследования бегают на беговой дорожке (настроенной с одинаковой скоростью) столько, сколько они могут, и продолжительность бега собирается для каждого человека.
Еще до сбора данных мы можем определить схему, которая выражает наши ожидания относительно положительного результата:
import pandas as pd
from pandera import Hypothesis
endurance_study_schema = pa.DataFrameSchema({
"subject_id": Column(pa.Int),
"arm": Column(
pa.String,
Check.isin(["treatment", "control"])
),
"duration": Column(
pa.Float, checks=[
Check.greater_than(0),
Hypothesis.two_sample_ttest( # Рассчитайте t-критерий для средних значений двух выборок
# https://pandera.readthedocs.io/en/stable/generated/methods/pandera.hypotheses.Hypothesis.two_sample_ttest.html
sample1="treatment",
relationship="greater_than",
sample2="control",
groupby="arm",
alpha=0.01,
)
]
)
})
После того, как набор данных для этого исследования будет собран, мы можем пропустить его через схему, чтобы подтвердить гипотезу о том, что группа, принимающая препарат, увеличивает физическую выносливость, измеряемую продолжительностью бега.
Другой распространенной проверкой гипотез может быть проверка нормального распределения выборки. Используя функцию scipy.stats.normaltest
, можно написать:
import numpy as np
from scipy import stats
dataframe = pd.DataFrame({
"x1": np.random.normal(0, 1, size=100),
})
dataframe.head()
x1 | |
---|---|
0 | -1.335490 |
1 | 2.402950 |
2 | -1.702813 |
3 | 0.085724 |
4 | -0.668640 |
schema = pa.DataFrameSchema({
"x1": Column(
checks=Hypothesis(
test=stats.normaltest,
# нулевая гипотеза: x1 нормально распределено
relationship=lambda k2, p: p > 0.01
)
),
})
schema(dataframe)
x1 | |
---|---|
0 | -1.335490 |
1 | 2.402950 |
2 | -1.702813 |
3 | 0.085724 |
4 | -0.668640 |
... | ... |
95 | -0.870995 |
96 | 0.037310 |
97 | 1.008274 |
98 | -1.372855 |
99 | -1.376724 |
100 rows × 1 columns
Если мы хотим проверить значения одного столбца, связанного с другим, мы можем указать имя другого столбца в аргументе groupby
. Это изменяет ожидаемую сигнатуру функции Check
для входного словаря, где ключи представляют собой уровни дискретных групп в условном столбце, а значения представляют собой объекты Series
pandas, содержащие подмножества интересующего столбца.
Возвращаясь к примеру исследования выносливости, мы могли бы просто утверждать, что средняя продолжительность бега в экспериментальной группе больше, чем в контрольной группе, без оценки статистической значимости:
simple_endurance_study_schema = pa.DataFrameSchema({
"subject_id": Column(pa.Int),
"arm": Column(
pa.String,
Check.isin(["treatment", "control"])
),
"duration": Column(
pa.Float, checks=[
Check.greater_than(0),
Check(
lambda duration_by_arm: (
duration_by_arm["treatment"].mean() > duration_by_arm["control"].mean()
),
groupby="arm"
)
]
)
})